package concordia.comp6231.server;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.net.InetAddress;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import org.omg.CORBA.ORB;
import org.omg.CosNaming.NamingContextExt;
import org.omg.CosNaming.NamingContextExtHelper;

import concordia.comp6231.boxOfficeService.MyService;
import concordia.comp6231.boxOfficeService.MyServiceHelper;
import concordia.comp6231.boxOfficeService.MyServicePOA;
import concordia.comp6231.model.Customer;
import concordia.comp6231.model.Reservation;
import concordia.comp6231.model.Show;


public class BoxOfficeImpl extends MyServicePOA {
	
	public final int EXCHANGE_UDP_PORT = 1813;
	public final int BOX_OFFICE_ID_LENGTH = 3;
	private final String INVALID_SHOW    = "Requested showID is not available.";
	private final String INVALID_TICKETS = "You do not the requested number of tickets.";
	
	private Map<String, Show> shows;
	private Map<String, String> exchangeTransactions;
	private String showsBaseDirectory;
	private String boxOfficeID;
	/**
	 * Used to to lock different thread to access a resource at the same time.
	 */
	private Object lock = new Object();
	private ORB orb;

	public void setORB(ORB orb_val) {
		orb = orb_val;
	}	

	public BoxOfficeImpl() {
		shows = new HashMap<String, Show>();
		exchangeTransactions = new HashMap<String, String>();
		// Create and start a thread for the UDP server 
		new ExchangeUDPServer(EXCHANGE_UDP_PORT, this).start();
	}
	
	public String getShowsBaseDirectory() {
		return showsBaseDirectory;
	}

	public void setShowsBaseDirectory(String showsBaseDirectory) {
		if (showsBaseDirectory.endsWith("\\") || showsBaseDirectory.endsWith("/")) {
			this.showsBaseDirectory = showsBaseDirectory.substring(0, showsBaseDirectory.length() - 1) + "/";
		} else {
			this.showsBaseDirectory = showsBaseDirectory  + "/";
		}
		loadShows();
	}

	public void addShow(String showID, Show show) {
		shows.put(showID, show);
	}

	public Show getShow(String showID)  {
		return shows.get(showID);
	}

	public Map<String, Show> listShows() {
		return Collections.unmodifiableMap(shows);
	}

	public void setBoxOfficeID(String boxOfficeID) {
		if (boxOfficeID == null || boxOfficeID.length() != BOX_OFFICE_ID_LENGTH) {
			throw new IllegalArgumentException("Invalid boxOfficeID value, boxOfficeID should be a String of length " + BOX_OFFICE_ID_LENGTH);
		}
		this.boxOfficeID = boxOfficeID;
	}
	
	public String getBoxOfficeID() {
		return boxOfficeID;
	}
	
	private void loadShows() {
		File showsDir;
		showsDir = new File(showsBaseDirectory);
		if (showsDir.isDirectory()) {
			File[] files = showsDir.listFiles();
			for (File showFile: files) {
				if (showFile.getName().startsWith("Show_" + this.getBoxOfficeID()) && showFile.isFile() ) {
					parseShowsData(showFile);
				}
			}
		}
	}
	
	private void parseShowsData(File showFile) {
		System.out.println("Loading Show: " + showFile.getAbsolutePath());
		String showID = showFile.getName().substring(5, 11);
		Show show = new Show(showID);
		shows.put(showID, show);
		try {
			BufferedReader reader = new BufferedReader(new FileReader(showFile));
			String line;
			while ((line = reader.readLine()) != null) {
				String[] record = line.split(",");
				Customer customer = new Customer(record[0]);
				show.addReservation(customer, Integer.parseInt(record[1]));
			}
			reader.close();
		} catch (Exception e) {
			System.err.format("Error reading file: '%s'.", showFile.getName());
			e.printStackTrace();
		}
	}
	
	private void saveShow(Show show) {
		String fileName = showsBaseDirectory + "Show_" + show.getShowID() + ".dat";
		File file;
		FileWriter fw = null;
		try {
			file = new File(fileName);
			fw = new FileWriter(file, false);
			for (Reservation reservation : show.reservations()) {
				fw.write(reservation.getCustomer().getCustomerID() + "," + reservation.getNumberOfTickets() + "\r\n");
			}
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			try {
				fw.flush();
				fw.close();
				lock.notifyAll();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
	
	@Override
	public int check(String showID) {
		System.out.println("Check action receievd for showID: " + showID);
		if (ReplicaMember.currentInstance().isLeader()) {
			BoxOfficeBroadcast boxOfficeBroadcast = new BoxOfficeBroadcast();
			boxOfficeBroadcast.check(showID);
		}
		if (!shows.containsKey(showID)) {
			throw new IllegalStateException(INVALID_SHOW);
		}
		Show show = shows.get(showID);
		return show.getAvailableTicketsCount();
	}
	
	@Override
	public void reserve(String customerID, String showID, int numberOfTickets) {
		System.out.println("Reserve action receievd: customerID=" + customerID + ", showID=" + showID + ", numberOfTickets=" + numberOfTickets);
		if (ReplicaMember.currentInstance().isLeader()) {
			BoxOfficeBroadcast boxOfficeBroadcast = new BoxOfficeBroadcast();
			boxOfficeBroadcast.reserve(customerID, showID, numberOfTickets);
		}
		if (!shows.containsKey(showID)) {
			throw new IllegalStateException(INVALID_SHOW);
		}
		Customer customer = new Customer(customerID);
		Show show = shows.get(showID);
		synchronized (lock) {
			show.addReservation(customer, numberOfTickets);
			saveShow(show);
		}
	}

	@Override
	public void cancel(String customerID, String showID, int numberOfTickets) {
		System.out.println("Cancel action receievd: customerID=" + customerID + ", showID=" + showID + ", numberOfTickets=" + numberOfTickets);
		if (ReplicaMember.currentInstance().isLeader()) {
			BoxOfficeBroadcast boxOfficeBroadcast = new BoxOfficeBroadcast();
			boxOfficeBroadcast.cancel(customerID, showID, numberOfTickets);
		}
		if (!shows.containsKey(showID)) {
			throw new IllegalStateException(INVALID_SHOW);
		}
		Customer customer = new Customer(customerID);
		Show show = shows.get(showID);
		synchronized (lock) {
			show.cencelReservation(customer, numberOfTickets);
			saveShow(show);
		}
	}

	@Override
	public void exchange(String customerID, String reservedShowID, int reservedTickets, String desiredShowID, int desiredTickets) {
		System.out.println("Exchange action receievd");
		if (ReplicaMember.currentInstance().isLeader()) {
			BoxOfficeBroadcast boxOfficeBroadcast = new BoxOfficeBroadcast();
			boxOfficeBroadcast.exchange(customerID, reservedShowID, reservedTickets, desiredShowID, desiredTickets);
		}
		// txID: 000001-MTL001-5-QUE001-2
		StringBuilder sb = new StringBuilder();
		sb.append(customerID).append("-")
		.append(reservedShowID).append("-")
		.append(reservedTickets).append("-")
		.append(desiredShowID).append("-")
		.append(desiredTickets);
		String transactionID = sb.toString();
		exchangeTransactions.put(transactionID, "NEW");

		// 1- MTL: Check that user 000001 has 5 tickets in reservation for show MTL001
		if (!shows.containsKey(reservedShowID)) {
			exchangeTransactions.remove(transactionID);
			throw new IllegalStateException(INVALID_SHOW);
		}
		if (customerReservations(customerID, reservedShowID) < reservedTickets) {
			exchangeTransactions.remove(transactionID);
			throw new IllegalStateException(INVALID_TICKETS);
		}
		
		exchangeTransactions.put(transactionID, "RESERVED_AVAILABLE");
		// 2- MTL: Send a check request to QUE to know if show QUE001 has 2 available tickets, and to get it's IP
		String desiredBoxOffice = desiredShowID.substring(0, 3);
		org.omg.CORBA.Object objRef;
		String objName = desiredBoxOffice;
		MyService serviceImpl = null;
		try {
			objRef = orb.resolve_initial_references("NameService");
			NamingContextExt ncRef = NamingContextExtHelper.narrow(objRef);
			serviceImpl = MyServiceHelper.narrow(ncRef.resolve_str(objName));
		} catch (Exception e) {
			e.printStackTrace();
			exchangeTransactions.remove(transactionID);
			throw new IllegalStateException(e.getMessage());
		}
		
		// 3- MTL: if all ok, send a UDP packet to QUE to ask for the exchange
		String desiredHostName = serviceImpl.getHostname();
		String exchangeResponse = null;
		try {
			exchangeResponse = UDPClient.send(desiredHostName, EXCHANGE_UDP_PORT, "canExchange-" + transactionID);
		} catch (Exception e) {
			e.printStackTrace();
			exchangeTransactions.remove(transactionID);
			throw new IllegalStateException(e.getMessage());
		}
		
		if (exchangeResponse == null || !exchangeResponse.startsWith("canExchange")) {
			exchangeTransactions.remove(transactionID);
			throw new IllegalStateException(exchangeResponse);
		}
		// 7- MTL: cancel 5 MTL001 shows for user 000001
		try {
			exchangeTransactions.put(transactionID, "DESIRED_SUCCESS");
			this.cancel(customerID, reservedShowID, reservedTickets);
			exchangeTransactions.remove(transactionID);
		} catch (Exception e) {
			// send a cancel to the QUE server to reservse the last action 
			try {
				exchangeResponse = UDPClient.send(desiredHostName, EXCHANGE_UDP_PORT, "rollBack-" + transactionID);
			} catch (Exception ex) {
				ex.printStackTrace();
				exchangeTransactions.remove(transactionID);
				throw new IllegalStateException(e.getMessage());
			}
		}		
	}

	private int customerReservations(String customerID, String showID) {
		return shows.get(showID).getCustomerReservations(customerID);
	}

	@Override
	public String getHostname() {
		// this action need never be braodcasted
		String computerName = "";
		try {
		    computerName = InetAddress.getLocalHost().getHostName();
		} catch(Exception e) {
			throw new IllegalStateException(e.getMessage());
		}
		return computerName;
	}
	
	public boolean showExists(String showID) {
		return shows.containsKey(showID);
	}
	
}
